Cross-platform App Hardening (DexGuard-class), Enterprise-gated - #5527
Cross-platform App Hardening (DexGuard-class), Enterprise-gated#5527shai-almog wants to merge 111 commits into
Conversation
Adds a single hardening layer that renames classes/methods/fields, encrypts string constants and obfuscates control flow across every port (Android, iOS/ ParparVM, JavaScript, native desktop) from one bytecode transform, integrated with Crash Protection so obfuscated stack traces are still symbolicated. Engine (new maven/cn1-hardening, run as a forked process so it is single-sourced with the build daemon and carries its own ProGuard/ASM): demux the fat jar, rename with ProGuard using a prefixed dictionary that avoids the ParparVM NativeSymbolIndex culler pathology, encrypt LDC literals and static-final ConstantValue strings with a per-class decoder, opaque-predicate control flow on safe platforms, ParparVM mangle-collision guard, CheckClassAdapter verification, and a cross-platform mapping. Android keeps R8 as its sole renamer. Symbolication (new maven/cn1-retrace): ProGuard mapping parse/chain plus the ParparVM trace-string parser that java.lang.Throwable.getStackTrace() now mirrors on device, and a local retrace CLI. Crash payload gains rawStack/traceFormat/ mappingId/hardenLevel; PiiScrubber.scrubRawStack; cause-chain capture. Surface/entitlement: harden.* build hints, HardeningPreflight (fail the build on local/source targets, invalid level, on-device-debug), Executor.hardenSourceJar/ runBuild wiring, a read-only Hardening status API, and the App-Hardening developer guide chapter. Also fixes the invalid build_key literal, the BuildHintEditor grouped-Select values lookup, and the "obfuscates by default" overclaim in the security chapter. Tests: 25 unit tests across the two modules and the crash payload (full ProGuard round-trip, string round-trip + plaintext-absence, control-flow verification, mapping retrace, trace-format detection, pre-flight truth table). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ae40837827
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
✅ Continuous Quality ReportTest & Coverage
Static Analysis
Generated automatically by the PR CI workflow. |
There was a problem hiding this comment.
Pull request overview
Introduces an Enterprise-gated, cross-platform “App Hardening” pipeline (bytecode-level renaming + string encryption + optional control-flow obfuscation) and integrates it with Crash Protection so hardened builds can still be symbolicated using a retained mapping.
Changes:
- Adds new Maven modules:
cn1-hardening(engine) andcn1-retrace(mapping/trace parsing + retrace CLI). - Integrates hardening into the Maven plugin build flow (preflight checks + engine invocation + platform IDs), and updates Crash Protection payload schema to include
rawStack/traceFormat/mappingId/hardenLevel. - Improves ParparVM
Throwable.getStackTrace()by parsing the native stack string into structured frames.
Reviewed changes
Copilot reviewed 54 out of 55 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vm/JavaAPI/src/java/lang/Throwable.java | Parse ParparVM stack text into StackTraceElements |
| tests/core/test/com/codename1/crash/CrashReportPayloadTest.java | Tests for traceFormat + hardening fields JSON |
| Ports/JavaSE/src/com/codename1/impl/javase/BuildHintSchemaDefaults.java | Adds hardening build-hint schema defaults |
| Ports/JavaSE/src/com/codename1/impl/javase/BuildHintEditor.java | Fix grouped Select .values resolution |
| maven/pom.xml | Adds hardening/retrace modules; bumps ProGuard |
| maven/codenameone-maven-plugin/src/test/java/com/codename1/maven/HardeningPreflightTest.java | Unit tests for hardening preflight matrix |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/HardeningPreflight.java | Client-side preflight validation logic |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/maven/CN1BuildMojo.java | Invoke preflight; route local builds via runBuild |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/WindowsNativeBuilder.java | Provides hardening platform id |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/LinuxNativeBuilder.java | Provides hardening platform id |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/JavaScriptBuilder.java | Provides hardening platform id |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/IPhoneBuilder.java | Provides hardening platform id |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java | Fork/execute hardening engine; mappingId/buildKey handling |
| maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/AndroidGradleBuilder.java | Android: disable engine renaming; mappingId + encoded build_key |
| maven/codenameone-maven-plugin/pom.xml | Embeds shaded hardening jar into plugin resources |
| maven/cn1-retrace/src/test/java/com/codename1/retrace/ParparVmTraceParserTest.java | Golden tests for ParparVM trace parser |
| maven/cn1-retrace/src/test/java/com/codename1/retrace/MappingFileTest.java | Tests for mapping parse + retrace + chaining |
| maven/cn1-retrace/src/main/java/com/codename1/retrace/RetraceMain.java | Standalone retrace CLI entrypoint |
| maven/cn1-retrace/src/main/java/com/codename1/retrace/ParparVmTraceParser.java | Parses ParparVM text traces; rejects JS stacks |
| maven/cn1-retrace/src/main/java/com/codename1/retrace/MappingFile.java | Parses/inverts ProGuard mapping for retrace |
| maven/cn1-retrace/src/main/java/com/codename1/retrace/MappingChain.java | Applies multiple mappings in order |
| maven/cn1-retrace/src/main/java/com/codename1/retrace/Frame.java | Frame DTO for retrace pipeline |
| maven/cn1-retrace/pom.xml | Module build + shaded standalone jar |
| maven/cn1-hardening/src/test/java/com/codename1/hardening/StringEncryptTransformTest.java | End-to-end string encryption tests |
| maven/cn1-hardening/src/test/java/com/codename1/hardening/fixture/Secrets.java | Fixture class for encryption tests |
| maven/cn1-hardening/src/test/java/com/codename1/hardening/fixture/Helper.java | Fixture class for rename tests |
| maven/cn1-hardening/src/test/java/com/codename1/hardening/ControlFlowTransformTest.java | Control-flow guard verification tests |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/StringEncryptTransform.java | ASM transform: encrypt LDC + ConstantValue strings |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/ProGuardRunner.java | Programmatic ProGuard rename runner |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/OutputVerifier.java | CheckClassAdapter verification gate |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/MappingWriter.java | Mapping header + mappingId computation |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/MangleCollisionCheck.java | Guards ParparVM symbol mangle collisions |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/Main.java | Engine CLI front-end + exit codes |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/JarDemuxer.java | Split/rebuild fat jar around class entries |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/InputJarKeepScanner.java | ASM scan for reflective keep rules |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningResult.java | Engine result model |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningRequest.java | Engine request model |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningProfile.java | Hardening levels + defaults |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningException.java | Engine exception type |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningEngine.java | Pipeline orchestration + safety gates |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningConfig.java | Parses harden.* hints into config |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/ControlFlowTransform.java | ASM opaque-predicate entry guards |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/Cn1NameFactory.java | zq-prefixed dictionary generator |
| maven/cn1-hardening/src/main/java/com/codename1/hardening/BuiltinKeepRules.java | Tier-1 keep rules + shared R8 rules |
| maven/cn1-hardening/pom.xml | Engine module deps + shaded standalone jar |
| docs/developer-guide/security.asciidoc | Correct obfuscation claims; reference hardening |
| docs/developer-guide/developer-guide.asciidoc | Include App-Hardening chapter |
| docs/developer-guide/Crash-Protection.asciidoc | Document new crash payload fields + hardening flow |
| docs/developer-guide/App-Hardening.asciidoc | New hardening chapter |
| CodenameOne/src/com/codename1/security/hardening/package-info.java | Package docs for Hardening API |
| CodenameOne/src/com/codename1/security/hardening/Hardening.java | Read-only hardening status API |
| CodenameOne/src/com/codename1/crash/PiiScrubber.java | Add scrubRawStack hook |
| CodenameOne/src/com/codename1/crash/CrashReportPayload.java | Add rawStack/traceFormat/mappingId/hardenLevel |
| CodenameOne/src/com/codename1/crash/CrashProtection.java | Capture/scrub rawStack; include in payload |
Suppressed comments (2)
maven/cn1-hardening/src/main/java/com/codename1/hardening/HardeningConfig.java:99
- Any unrecognized harden.strings value currently falls into the final else branch and enables maximum string encryption. This can turn on expensive/behaviour-changing encryption accidentally (e.g. a typo), so the parser should only treat explicit "all"/"true"/"2"/"3" as 'all' and otherwise fall back to the level defaults (or fail).
} else {
// "all", "true", "2", "3"
encConst = true;
encAll = true;
}
docs/developer-guide/Crash-Protection.asciidoc:93
- This paragraph states that on ParparVM ports the Java trace arrives only as rawStack and is parsed server-side. With Throwable.getStackTrace() now parsing the ParparVM stack string into structured frames on-device, the server may receive both; consider updating this sentence to reflect that rawStack is still
parparvm-textbut frames may be structured too (and rawStack may primarily be for the cause-chain / verbatim trace).
When a build is hardened (see <<app-hardening>>), the build server retains the cross-platform obfuscation mapping and symbolicates incoming reports against it, so a hardened build's crashes still land as readable, correctly-lined issues. Two things follow from how the mapping is retained: a report whose mapping has aged out of retention can no longer be retraced, and a locally hardened build -- whose mapping never reached the server -- can't be symbolicated at all. On the ParparVM ports (iOS, tvOS, watchOS, mac-native, Windows, Linux) the Java trace arrives as `rawStack` in the `parparvm-text` format and is parsed server-side; on the JavaScript port it arrives as a JavaScript engine stack (`js-error`) and is symbolicated best-effort through the source map.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
✅ ByteCodeTranslator Quality ReportTest & Coverage
Benchmark Results
Static Analysis
Generated automatically by the PR CI workflow. |
Cloudflare Preview
|
- Complete GPLv2+Classpath header on the 4 files the copyright gate flagged (BuildHintEditor had none; BuildHintSchemaDefaults + the two new tests were short). - Convert Hardening.java/package-info.java to /// markdown comments (core src gate). - Declare cn1-hardening:standalone as a runtime-scope plugin dependency so the reactor builds the engine before the plugin embeds it (fixes the antrun copy failing in CI); fix an illegal -- inside the new XML comment. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ProGuard 7.3.2 cannot read class files newer than JDK 20 (it fails on the JDK's own module classes), so the renamer must run on JDK 8-20 -- the cloud daemon forks the engine on JDK 17. The engine now fails with a clear message instead of a cryptic ProGuard error when renaming is requested on a too-new JVM, and the ProGuard-dependent tests skip (JUnit assumption) on JDK 21+ so the PR CI JDK-21 leg stays green. String encryption and control-flow tests have no such limit. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: fc8822afde
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
- FrameClassWriter: COMPUTE_FRAMES resolved common superclasses through the engine's own classloader, which lacks the app/library classes when run as a forked jar, so any class with a merge between application types aborted hardening. Resolve the hierarchy from a classloader over the (renamed) input classes plus the library jars, falling back to Object. Threaded through the string-encryption and control-flow transforms; unit-tested. (Codex P1) - Pass the FULLY QUALIFIED main class to the keep rules: getMainClass() is the simple name, so a bare value kept a default-package class and let ProGuard rename the real application class out from under the generated stub. Fixed in both the plugin and daemon config writers. (Codex P1) - The reactor dependency forcing cn1-hardening to build before the plugin (so the engine jar exists for the embed step) already landed in the prior commit. (Codex P1) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated no new comments.
Suppressed comments (3)
Ports/JavaSE/src/com/codename1/impl/javase/BuildHintEditor.java:206
- Select-hint value parsing uses the last character of the values string as the delimiter (
charAt(len-1)), which breaks all of the comma-separated.valuesentries coming fromBuildHintSchemaDefaults(e.g.off,standard,aggressive,paranoidwill split ond). This prevents the new Hardening Select hints (and existing ones likenativeTheme) from populating correctly.
String separator = ""+valuesString.charAt(valuesString.length()-1);
ArrayList<String> values = new ArrayList<String>();
values.add("");
for (String value : valuesString.split(separator)) {
docs/developer-guide/Crash-Protection.asciidoc:86
- This section says ParparVM ports rely on
rawStackbecausegetStackTrace()yields only a formatted string, but this PR updatesvm/JavaAPI/java/lang/Throwable.getStackTrace()to parse the ParparVM text stack into structuredStackTraceElement[]. The docs should be updated to reflect that structured frames are now available on ParparVM (andrawStackis supplemental / for the full rendered text and cause chain).
- `frames[]` -- class / method / file / line / `native` flag per frame
- `rawStack` -- the pre-rendered Java stack (via `printStackTrace`, including the cause chain). On the ParparVM ports this is the readable Java trace, since `getStackTrace()` there yields a formatted string rather than structured frames
- `traceFormat` -- how the server should read `rawStack`: `structured`, `parparvm-text`, `js-error`, or `none`. Derived, never guessed
maven/codenameone-maven-plugin/src/main/java/com/codename1/builders/Executor.java:2457
- The generic
catch (Exception e)wraps the error without the original cause, which makes diagnosing hardening failures much harder (stack trace is lost). Preserve the cause in theBuildException.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: efa163b074
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 57 changed files in this pull request and generated no new comments.
Suppressed comments (2)
vm/JavaAPI/src/java/lang/Throwable.java:144
- setStackTrace() updates parsedStack, but printStackTrace() prints the pre-rendered "stack" string field. Because setStackTrace() doesn't update "stack", callers that set a custom stack trace will still see the old/empty stack printed (and CrashProtection.safeRawStack() relies on printStackTrace()). Rebuild the "stack" string from the provided StackTraceElement[] so printStackTrace() reflects the updated trace.
CodenameOne/src/com/codename1/crash/CrashReportPayload.java:120 - CrashReportPayload documents hardenLevel as one of "off"/"standard"/"aggressive"/"paranoid", but it currently defaults to the empty string when the property isn't set. This makes the payload ambiguous for unhardened builds and diverges from Hardening.getLevel() which defaults to "off".
this.osVersion = d.getProperty("OSVer", "");
this.mappingId = d.getProperty("cn1.mappingId", "");
this.hardenLevel = d.getProperty("cn1.hardenLevel", "");
Locale loc = Locale.getDefault();
|
Developer Guide build artifacts are available for download from this workflow run:
Developer Guide quality checks: |
Codex P1/P2: - harden.keep: split on newlines only (a ';' is legal inside a rule body). - Keep SourceFile,LineNumberTable so ParparVM/native traces keep real line numbers for retrace. - Honor constants-vs-all string mode: 'constants' encrypts only values declared as static-final String constants (and javac's inlined copies), 'all' encrypts every literal. - Propagate cn1.mappingId/cn1.hardened/cn1.hardenLevel into the request before stub generation; Android stub now stamps them (Hardening.isHardened(), crash report mappingId/level). - Supply the compile/platform classpath to ProGuard as library jars so an app method overriding a framework method is not renamed apart from its superclass. - Append harden.keep + the name-bound PropertyBusinessObject keep to Android's R8 config (Android keeps R8 as sole renamer). Build fixes: - CrashProtection.safeRawStack: build the raw stack with StringBuilder instead of java.io.PrintWriter, which the core's CLDC11 bootclasspath (ANT build) lacks. - Embed the engine jar via maven-dependency-plugin:copy (resolves the standalone artifact from the reactor/repo) so partial plugin-only CI builds no longer fail copying from an unbuilt sibling target/. - Keep the test resource bytes ASCII (explicit byte[] rather than a non-ASCII literal). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 605f722c41
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…, Select delimiters) - OutputVerifier: pass the input/library hierarchy classloader to CheckClassAdapter.verify so the final verification pass resolves application types instead of loading them from the engine's classpath (a class with a merge between app types would otherwise fail verification). (P1) - Keep every class named by a META-INF/services/* descriptor (the service interface and each provider), since the descriptors are copied verbatim and ServiceLoader would break if they were renamed; regression-tested. (P1) - Terminate the hardening Select .values lists with their delimiter, which BuildHintEditor reads as the last character, so the simulator shows the real options instead of splitting on a letter. (P2) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Compared 12 screenshots: 12 matched. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0fffb4c59f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 57 out of 57 changed files in this pull request and generated no new comments.
Suppressed comments (5)
maven/cn1-hardening/src/main/java/com/codename1/hardening/JarDemuxer.java:120
rebuild()doesn’t close itsZipOutputStream. Even with an explicitfinish(), closing the stream is the reliable way to release the deflater/native resources and ensure the central directory is flushed.
FileOutputStream fo = new FileOutputStream(outJar);
try {
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fo));
for (Map.Entry<String, byte[]> e : classesByInternalName.entrySet()) {
ZipEntry entry = new ZipEntry(e.getKey() + ".class");
maven/cn1-hardening/src/main/java/com/codename1/hardening/JarDemuxer.java:145
readClasses()creates aZipInputStreambut never closes it. Closing only the underlyingFileInputStreamworks most of the time, but it’s safer and clearer to close the ZIP stream explicitly (try-with-resources).
FileInputStream fi = new FileInputStream(jar);
try {
ZipInputStream zis = new ZipInputStream(fi);
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
Ports/JavaSE/src/com/codename1/impl/javase/BuildHintEditor.java:201
propName.indexOf("}}.")is assumed to be present here. If it isn’t (unexpected/third-party schema key), the currentsubstring(... + 3)call will produce an incorrect key (and can throw if the key is shorter), andvaluesString.charAt(valuesString.length()-1)will also throw on an empty values string. Consider guarding the index/length and making the value-splitting robust (comma-separated lists in this repo don’t end with a delimiter).
String valuesKey = propName.substring(0, propName.indexOf("}}.")+3) + "values";
String valuesString = System.getProperty(valuesKey);
if (valuesString == null) {
valuesString = System.getProperty("codename1.arg.{{ "+model.name+" }}.values");
}
CodenameOne/src/com/codename1/crash/CrashReportPayload.java:147
deriveTraceFormat()can classify a raw stack asparparvm-texteven if the frame body contains spaces. That diverges from the on-device ParparVM parser logic (which rejects frames containing spaces) and could send the server down the wrong parse path for some JS engine stack formats.
int at = rawStack.indexOf(" at ");
if (at >= 0) {
int lineEnd = rawStack.indexOf('\n', at);
String body = lineEnd < 0 ? rawStack.substring(at + 7) : rawStack.substring(at + 7, lineEnd);
if (body.indexOf('(') < 0 && body.indexOf('/') < 0 && body.indexOf('@') < 0) {
maven/cn1-hardening/src/main/java/com/codename1/hardening/JarDemuxer.java:103
split()createsZipInputStream/ZipOutputStreambut never closes them. Relying on only closing the underlyingFileInputStream/FileOutputStreamcan leak native resources and (for the output) risks incomplete ZIP finalization on some JVMs/filesystems. Use try-with-resources to ensure ZIP streams are closed.
This issue also appears in the following locations of the same file:
- line 116
- line 141
ZipInputStream zis = new ZipInputStream(fi);
FileOutputStream fo = new FileOutputStream(classesJarOut);
try {
ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fo));
ZipEntry entry;
MappingWriter.jsonEscape only escaped quote and backslash, so a SourceFile containing a newline, tab or other control character was emitted literally -- a raw newline split the single-line metadata comment and a raw control char is invalid JSON, either of which stopped MappingFile.parseSourceFileMetadata from recovering the filename (retrace then fell back to a synthesized path). This is reachable for a Kotlin or package-private Java class in an unusually named Unix file. Escape b/f/n/r/t and other control chars (as the 4-hex-digit form), and teach the reader to decode those escapes back. Round-trip tested both sides. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 799372c730
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
parseSourceFileMetadata trim()'d the decoded fileName, so a Unix source file whose name has leading/trailing whitespace (e.g. " Screen.kt") retraced to a different, nonexistent filename and broke source links. The closing quote bounds the JSON string exactly, so return the decoded value verbatim and treat only a genuinely empty value as absent. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 374c2168b3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
A HealthBackgroundListener is reconstructed after a process restart by its persisted class name: HealthStore writes getClass().getName() to Preferences, and the platform builder scans the hardened jar to generate a factory mapping that name back to a constructor. The engine's keep set omitted it, so an engine-renamed Apple/native build renamed the implementor; because the default mapping seed changes between builds, an updated app's regenerated factory no longer recognized the previously persisted name and resolveBackgroundListener() silently returned nothing, deferring background health delivery. Keep the implementors, exactly as the location/background-callback and Login rules do. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 364d1101ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
applyHardeningRequestArgs was called only from the Android/iOS/JavaScript request builders; the Windows and Linux native builders (windows-source, local-windows-device, local-linux-device) built their request and called runBuild without it. So a local Windows/Linux build that took harden.allowUnhardenedLocalBuild=true injected no cn1.harden.forceOff and hardenSourceJar ran hardening anyway, producing an orphaned local mapping. Call the helper from those two request builders too; all five runBuild sites now carry the per-build decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Migrating a static-final String's ConstantValue into <clinit> changes initialization ordering: the ConstantValue is otherwise assigned during preparation, before any <clinit> runs, so a superclass whose <clinit> reads a subclass constant during a REENTRANT initialization observes the value, whereas a <clinit>-assigned field is still null at that reentrant point. javac and kotlinc both inline compile-time String constant reads, so this only arises for generated bytecode that emits a real GETSTATIC. Scan the jar for such reads and preserve those fields' ConstantValue (plaintext, disclosed), excluding the value jar-wide so an equal LDC elsewhere is not encrypted+interned and broken == against it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…limit FrameClassWriter: an interface-involving frame merge resolves to Object and is deliberately NOT flagged incomplete even under a partly-readable interface hierarchy -- the verifier treats every class as assignable to every loadable interface (JVMS 4.10.1.2), so a later invokeinterface still verifies; flagging it would only needlessly un-harden a valid class. Document the reasoning at the early return. JarDemuxer: resources are copied under their original path, so a package- relative getResourceAsStream from a renamed class misses them. This is a deliberate choice -- CN1's resource model uses absolute paths and package names are obfuscated on purpose -- so document it (and the harden.keep escape hatch for a bundled dependency that loads a package-relative resource) at the copy site and in App-Hardening.asciidoc. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9757e155cb
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The App-Hardening resource-limitation note tripped Microsoft.Adverbs on "rarely"; reword without the adverb. No content change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fb8161ef0
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
FrameClassWriter: the superclass walk returned java/lang/Object via isAssignableFromBytes(Object, type2) -- always true -- BEFORE the chainReachesObject incompleteness check, so a merge where type2's chain is broken by a missing intermediate that hides a nearer common base (A extends Base, B extends an absent Missing extends Base) was emitted as Object without being flagged. Stop the walk before Object so reaching it falls through to the incompleteness check; a broken type2 chain is now flagged and the class shipped unhardened. StringEncryptTransform: the GETSTATIC collector recorded the reference owner (C.X), but an inherited field read (GETSTATIC C.X for a field declared by superclass B) must key on the declaring class B.X, which is what encryptStaticFinalStrings checks. Resolve the owner to the field's declaring class through the hierarchy before recording it, so the ConstantValue of an inherited, externally-read constant is preserved rather than migrated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deriveTraceFormat labeled any non-JavaScript raw stack containing a " at X.Y:N" line (no parens) as parparvm-text purely by shape. An Android/desktop (real-JVM) throwable whose MESSAGE contains such a line -- printStackTrace echoes the message into the raw stack -- was then misclassified, letting the server fabricate a frame from message text or drop a real cause trace. Gate the classification on an actual ParparVM-C runtime platform (ios/mac/linux/win); a real-JVM stack now stays none regardless of message contents. The shape check still guards an unexpected stack on a C target. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1bf1604ca7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
JavaSEPort.getPlatformName() returns mac/win for a skinless desktop app (Linux falls through to win), which collide with the native ParparVM-C names, so the platform-name gate still misclassified a JavaSE desktop throwable whose message contained a " at X.Y:N" line as parparvm-text. ParparVM's System.getProperty always returns null while a real JVM sets java.vm.name, so derive the trace format from that runtime signal: anything on a JVM (JavaSE desktop, the simulator, or Android) is never parparvm-text regardless of its displayed name. A package-private 4-arg overload takes runningOnJvm explicitly so a JVM-hosted unit test can still exercise the native path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d4cc1541b6
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
OutputVerifier handled the case where CheckClassAdapter.verify THROWS on an absent target-only type, but ASM sometimes catches that failure internally and prints its stack trace to the report instead. A non-empty report was then rejected, so a class whose frame analysis needs an unavailable platform superclass could still fail hardening. Detect the unresolved-type text in the report (ClassNotFoundException / TypeNotPresentException / NoClassDefFoundError / " not present") and take the structural fallback, mirroring BytecodeComplianceMojo's isUnresolvableTypeOutput. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The standalone retrace CLI opened mapping files with FileReader (platform default charset), corrupting Unicode class/method/source-file names on a non-UTF-8 host such as Windows Java 8. The rest of the pipeline treats mappings as UTF-8 (MappingWriter reads/writes UTF-8), so read them with an explicit UTF-8 reader for consistent symbolication across hosts. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4124a5e38e
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The opening paragraph still described the old "mask only non-frame lines" behavior and promised a minified-JS coordinate like app.js:1:123456 survives, contradicting the accurate explanation later in the same javadoc. scrubRawStack now routes every line through scrubMessage, so document the uniform masking and its loss of large columns (short line numbers still survive; precise coordinates come from structured frames). No behavior change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ffe1519a3c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…cuit The Android APK cache check (skip build when the APK is newer than the sources) returned before applyHardeningPreflight() ran, and getSourcesModificationTime() keys only on source timestamps -- not build hints -- so an explicit hardening request, especially one made via a -D command-line property, was silently dropped and the stale, potentially unhardened APK returned with no error. Move the preflight ahead of the cache check and overlay -D command-line hints in the early preflight so the request is seen, validated, and refused if unsupported rather than silently ignored. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
OutputVerifier classified an entire CheckClassAdapter report as an unresolved-type failure by substring, but ASM can append both a missing-type diagnostic (one method) and a genuine analyzer error (another method) to the same report; the structural fallback then discarded the real error and shippable invalid bytecode. Verify structure once, then run SimpleVerifier data-flow PER METHOD: a method that fails only because a target-only type is absent is tolerated, while any other analyzer failure fails the build, naming the method. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8b6485ffc1
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Moving the pre-flight ahead of the cache check validated the hardening request but did not invalidate the cache, so an off->standard change made through a -D hint (which getSourcesModificationTime cannot see) still returned the older, possibly unhardened APK. Record whether the pre-flight resolved that hardening will run (non-off level and not force-off) and skip the source-timestamp cache short-circuit in that case, forcing a rebuild so the request is honored. When hardening is off / force-off / opted out, the cache still applies. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 760804329f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The previous guard only bypassed the up-to-date cache for off->on; disabling
hardening (on->off, opting Android out, or turning every transform off) left
hardeningWillRun false, so the cache returned the old HARDENED APK -- an
artifact contradicting the current config. Record the hardening OUTCOME the APK
was built with ("unhardened" or "hardened:<level>") in a marker beside the APK,
and treat the cache as up-to-date only when that marker still matches. A change
in either direction now rebuilds, while two unhardened (or identical-level)
invocations still hit the cache.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4684e09cf5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The marker keyed only on harden.level, so changing harden.strings, harden.rename, harden.controlFlow, harden.keep or harden.seed without changing the level left the key identical (both aggressive builds were "hardened:aggressive") and the timestamp cache reused an APK built with the previous transforms and mapping. Fingerprint every effective codename1.arg.harden.* setting (sorted, SHA-256) into the hardened key so any transform/keep/seed change invalidates the cache; an unhardened build keeps its single key. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
What
Adds App Hardening — a single Enterprise-gated layer that renames classes/methods/fields, encrypts string constants, and obfuscates control flow across every port (Android, iOS/ParparVM, JavaScript, native desktop) from one bytecode transform, integrated with Crash Protection so obfuscated traces are still symbolicated.
Runs on the cloud build server; this repo carries the engine and the client surface. The matching BuildDaemon PR turns it on (entitlement gate + engine invocation + mapping upload).
Pieces
maven/cn1-hardening— forked-process ProGuard+ASM engine. Demux → ProGuard rename with azq-prefixed dictionary that avoids the ParparVMNativeSymbolIndexdead-code-culler pathology → string encryption (LDC andstatic finalConstantValue, per-class decoder) → opaque-predicate control flow (safe platforms) → mangle-collision guard →CheckClassAdapterverify → mapping with provenance +mappingId. Android keeps R8 as its sole renamer.maven/cn1-retrace— ProGuard mapping parse/chain + the ParparVM trace-string parser that on-deviceThrowable.getStackTrace()now mirrors, + a local retrace CLI.rawStack/traceFormat/mappingId/hardenLevelpayload fields,PiiScrubber.scrubRawStack, cause-chain capture.harden.*hints,HardeningPreflight(fails local/source targets, invalid level, on-device-debug),Executor.hardenSourceJar/runBuild, read-onlyHardeningstatus API, simulator hint schema, newApp-Hardening.asciidoc.build_keyliteral,BuildHintEditorgrouped-Select.valueslookup,security.asciidoc"obfuscates by default" overclaim, ParparVMgetStackTrace()(fixes today's empty-frames iOS crash reports).Tests
25 unit tests green across the two modules and the crash payload: full ProGuard round-trip + behaviour preservation, string round-trip + plaintext-absence, control-flow verification, mapping retrace + chaining, trace-format detection (incl. V8/SpiderMonkey JS rejection), and the pre-flight truth table. Core, plugin, and javase compile.
Follow-ups (not blocking)
Device end-to-end retrace and the "did obfuscation break the app" matrix need a real cloud build. Android R8
-applymapping/keep export,parparvm-symbols.txtnative-address symbolication, daemon runtime stamping ofcn1.mappingId/cn1.hardened,HardeningVerifier(Check 3), and SpotBugs-gate wiring for the new modules are noted for later.Pairs with the BuildDaemon
app-hardeningPR.🤖 Generated with Claude Code